#include "opencl.h"

#include <QTextStream>
#include <QFile>
#include <QCoreApplication>

#include <QDebug>

#include <exception>
#include <stdexcept>

/// Parameter: platform: OpenCL platform to be used
///            device: device on the platform to be used
/// Return:
/// Action: Constructor
///         Sets OpenCL
OpenCL::OpenCL(const cl::Platform platform, const cl::Device device)
{
    _result = 0;
    _imageBuffer = 0;
    _resultBuffer = 0;
    _filterBuffer = 0;
    _queue = 0;

    _defaultPlatform = platform;

    _defaultDevice = device;

    _context = new cl::Context(_defaultDevice);

    QFile file(":/filter_kernel.cl");
    if (!file.open(QFile::ReadOnly))
    {
        std::runtime_error error("Error while opening the kernel file");
        throw error;

    }

    QTextStream stream(&file);
    _kernelCode = stream.readAll().toStdString();

    cl::Program::Sources sources;
    sources.push_back({_kernelCode.c_str(), _kernelCode.length()});

    _program = new cl::Program(*_context, sources);
    _error = _program->build({_defaultDevice});
    if (_error != CL_SUCCESS)
    {
        OpenCLException ex(_error, "Build error");
        throw ex;
    }

    _imgProcess =  new cl::Kernel(*_program, "filter_kernel", &_error);
    if (_error != CL_SUCCESS)
    {
        OpenCLException ex(_error, "Kernel error");
        throw ex;
    }
}

/// Parameter:
/// Return:
/// Action: Destructor
OpenCL::~OpenCL()
{
    delete _context;
    delete _imageBuffer;
    delete _resultBuffer;
    delete _filterBuffer;
    delete _queue;
    delete _program;
    delete _imgProcess;
    //DON'T DELETE _result !!! Buffer must remain valid for QImage
}

/// Parameter: data: array (pointer) containing the original image's pixel value
///            width: the original image's width
///            height: the original image's height
/// Return:
/// Action: Creates buffer for the data and the result.
///         CommandQueue is created here.
void OpenCL::set_image_buffer(int* data, const int width, const int height)
{
    int size = width * height;

    if (_imageBuffer != 0)
    {
        delete _imageBuffer;
        _imageBuffer = 0;
    }
    _imageBuffer = new cl::Buffer(*_context, CL_MEM_USE_HOST_PTR, sizeof(int) * size, data, &_error);
    if (_error != CL_SUCCESS)
    {
        OpenCLException ex(_error, "Buffer error");
        throw ex;
    }

    if (_result == 0)
        _result = new int[size];
    else
    {
        delete _result;
        _result = new int[size];
    }

    if (_resultBuffer != 0)
    {
        delete _resultBuffer;
        _resultBuffer = 0;
    }
    _resultBuffer = new cl::Buffer(*_context, CL_MEM_READ_WRITE, sizeof(int) * size, NULL, &_error);
    if (_error != CL_SUCCESS)
    {
        OpenCLException ex(_error, "Buffer error");
        throw ex;
    }

    _imageWidth = width;
    _imageHeight = height;

    if (_queue != 0)
    {
        delete _queue;
        _queue = 0;
    }

    _queue = new cl::CommandQueue(*_context, _defaultDevice, 0, &_error);
    if (_error != 0)
    {
        OpenCLException ex(_error, "CommandQueue error");
        throw ex;
    }
}

/// Parameter: data: filter matrix's array
///            size: filter matrix's size (width/height)
/// Return:
/// Action: Creates a buffer for the filter matrix.
void OpenCL::set_filter_buffer(float* data, const int size)
{
    if (_filterBuffer != 0)
    {
        delete _filterBuffer;
        _filterBuffer = 0;
    }
    _filterBuffer = new cl::Buffer(*_context, CL_MEM_USE_HOST_PTR, sizeof(float) * size * size, data, &_error);
    if (_error != CL_SUCCESS)
    {
        OpenCLException ex(_error, "Buffer error");
        throw ex;
    }

    _filterSize = size;
}

/// Parameter: bias: filter's bias parameter
/// Return:
/// Action: Sets bias.
void OpenCL::set_bias(const float bias)
{
    _bias = bias;
}

/// Parameter: bias: filter's factor parameter
/// Return:
/// Action: Sets factor.
void OpenCL::set_factor(const float factor)
{
    _factor = factor;
}

/// Parameter:
/// Return:
/// Action: Runs the kernel for the loaded data.
int* OpenCL::run_kernel()
{
   _error =  _imgProcess->setArg(0, *_imageBuffer);
   if (_error != CL_SUCCESS)
   {
       OpenCLException ex(_error, "SetArg error");
       throw ex;
   }

    _error = _imgProcess->setArg(1, _imageWidth);
    if (_error != CL_SUCCESS)
    {
        OpenCLException ex(_error, "SetArg error");
        throw ex;
    }

    _error = _imgProcess->setArg(2, _imageHeight);
    if (_error != CL_SUCCESS)
    {
        OpenCLException ex(_error, "SetArg error");
        throw ex;
    }

    _error = _imgProcess->setArg(3, *_resultBuffer);
    if (_error != CL_SUCCESS)
    {
        OpenCLException ex(_error, "SetArg error");
        throw ex;
    }

    _error = _imgProcess->setArg(4, _factor);
    if (_error != CL_SUCCESS)
    {
        OpenCLException ex(_error, "SetArg error");
        throw ex;
    }

    _error = _imgProcess->setArg(5, _bias);
    if (_error != CL_SUCCESS)
    {
        OpenCLException ex(_error, "SetArg error");
        throw ex;
    }

    _error = _imgProcess->setArg(6, *_filterBuffer);
    if (_error != CL_SUCCESS)
    {
        OpenCLException ex(_error, "SetArg error");
        throw ex;
    }

    _error = _imgProcess->setArg(7, _filterSize);
    if (_error != CL_SUCCESS)
    {
        OpenCLException ex(_error, "SetArg error");
        throw ex;
    }

    if (_imageWidth * _imageHeight < CL_DEVICE_MAX_WORK_GROUP_SIZE)
    {
        _error = _queue->enqueueNDRangeKernel(*_imgProcess, cl::NDRange(0), cl::NDRange(_imageWidth * _imageHeight));
        if (_error != CL_SUCCESS)
        {
            OpenCLException ex(_error, "EnqueueNDRangeKernel error");
            throw ex;
        }
    }
    else
    {
        int div = _imageWidth * _imageHeight / CL_DEVICE_MAX_WORK_GROUP_SIZE;
        int mod = _imageWidth * _imageHeight % CL_DEVICE_MAX_WORK_GROUP_SIZE;

        for (int i = 0; i < div; ++i)
        {
            if (!__canTransform)
                goto FREE_RESOURCE;

            _error = _queue->enqueueNDRangeKernel(*_imgProcess,
                                                 cl::NDRange(i * CL_DEVICE_MAX_WORK_GROUP_SIZE),
                                                 cl::NDRange(CL_DEVICE_MAX_WORK_GROUP_SIZE));
            if (_error != CL_SUCCESS)
            {
                OpenCLException ex(_error, "EnqueueNDRangeKernel error");
                throw ex;
            }

            _error = _queue->finish();
            if (_error != CL_SUCCESS)
            {
                OpenCLException ex(_error, "Finish error");
                throw ex;
            }

            emit progress(i * CL_DEVICE_MAX_WORK_GROUP_SIZE);
            QCoreApplication::processEvents(QEventLoop::AllEvents);
        }

        _error = _queue->enqueueNDRangeKernel(*_imgProcess,
                                             cl::NDRange(_imageWidth * _imageHeight - mod),
                                             cl::NDRange(mod));
        if (_error != CL_SUCCESS)
        {
            OpenCLException ex(_error, "EnqueueNDRangeKernel error");
            throw ex;
        }
    }

    _error = _queue->finish();
    if (_error != CL_SUCCESS)
    {
        OpenCLException ex(_error, "Finish error");
        throw ex;
    }

    _error = _queue->enqueueReadBuffer(*_resultBuffer, CL_TRUE, 0, sizeof(int) * _imageWidth * _imageHeight, _result);
    if (_error != CL_SUCCESS)
    {
        OpenCLException ex(_error, "EnqueueReadBuffer error");
        throw ex;
    }

    emit progress(_imageWidth * _imageHeight);

    return _result;

    // If the transformation has beeen canceled, resource must be freed.
    FREE_RESOURCE:
    delete [] _result;
    _result = 0;
    return 0;
}
